package com.jirclib;

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.LinkedList;
import java.net.Socket;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;

import com.jirclib.event.*;
import com.jirclib.listeners.*;

/**
 * Created by IntelliJ IDEA.
 * User: Aelin
 * Date: May 12, 2009
 * Time: 10:50:28 AM
 * To change this template use File | Settings | File Templates.
 */
/*
<message>  ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
<prefix>   ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
<command>  ::= <letter> { <letter> } | <number> <number> <number>
<SPACE>    ::= ' ' { ' ' }
<params>   ::= <SPACE> [ ':' <trailing> | <middle> <params> ]

<middle>   ::= <Any *non-empty* sequence of octets not including SPACE
               or NUL or CR or LF, the first of which may not be ':'>
<trailing> ::= <Any, possibly *empty*, sequence of octets not including
                 NUL or CR or LF>

<crlf>     ::= CR LF

 */
public class IRCConnection implements IRCConstants, Runnable {
    /**
     * The connection socket.
     */
    protected Socket socket;

    /**
     * Used for reading data from the remote server.
     */
    protected BufferedReader reader;

    /**
     * Used for sending data to the remote server.
     */
    protected PrintWriter writer;

    /**
     * Currently connected flag.
     */
    protected boolean connected = false;

    /**
     * This field specifies the server to connect to, or
     * is currently connected to.
     */
    protected String server = "";

    /**
     * This field specifies the remote server's port.
     */
    protected int port;
    
    /**
     * Current nickname, defaults to irclib_user.
     */
    protected String nickname = "irclib_user";

    /**
     * Current real name, defaults to John Doe.
     */
    protected String realName = "John Doe";

    /**
     * The current client version information.
     */
    protected String clientVersion = "jirclib";
    
    /**
     * List of currently registered event listeners.
     */
    protected LinkedList<IRCEventListener> listeners = new LinkedList<IRCEventListener>( );

    /**
     * Registers the specified listener.
     * 
     * @param listener Listener to register.
     */
    public void addListener( IRCEventListener listener ) {
        listeners.add( listener );
    }

    /**
     * Remove a previously registered IRCEventListener.
     * 
     * @param listener Listener to remove.
     */
    public void removeListener( IRCEventListener listener ) {
        listeners.remove( listener );
    }

    /**
     * This method sends a IRCEvent object to each registered
     * IRCEventListener in listeners for processing.
     * 
     * @param event Event to process.
     */
    public void sendEvent( IRCEvent event ) {
        // Pass this as a attachment.
        event.setAttachment( this );

        // Call each registered listener.
        for ( IRCEventListener listener : listeners ) {
            listener.eventReceived( event );
        }
    }

    /**
     * This method connects to the remote host and initializes
     * reader/writer objects.
     * 
     * @param server
     * @param port
     * @return
     */
    public boolean connect(String server, int port) {
        // Store connection details, if not previously stored.
        if ( !this.server.equalsIgnoreCase(server) ) this.server = server;
        if ( this.port != port ) this.port = port;

        //
        try {
            // Connect to server and create reader/writer.
            socket = new Socket(server, port);
            reader = new BufferedReader( new InputStreamReader(socket.getInputStream( )));
            writer = new PrintWriter( socket.getOutputStream( )) ;

            // Send connection details to server.
            sendString("NICK " + nickname);
            sendString("USER " + nickname + " 8 * :" + realName);

            // Set connected flag and return true.
            connected = true;
            return true;
        }
        catch ( Exception ex ) {
            return false;            
        }
    }

    /**
     * This method is used for determining if the IRC client
     * is still connected to the server.
     *
     * @return True if connected.
     */
    public boolean isConnected( ) {
        return connected;
    }

    /**
     * This method is the base send method, and is
     * generally only used internally.
     * 
     * @param message
     */
    public void sendString( String message ) {
        writer.println( message );
        writer.flush( );
    }

    public void sendNotice(String recipient, String message) {
        sendString("NOTICE " + recipient + " :" + message);
    }
    
    public void sendMessage(String recipient, String message) {
        sendString("PRIVMSG " + recipient + " :" + message);
    }

    public void sendCTCP(String recipient, String message) {
        sendMessage(recipient, "\u0001" + message + "\u0001");
    }

    /**
     * This method is used to join a single channel.
     * 
     * @param channel
     */
    public void join( String channel ) {
        join( new String[ ] { channel } );
    }

    /**
     * This method is used to join multiple channels.
     *
     * @param channels
     */
    public void join( String[ ] channels ) {
        join(channels, null);
    }

    /**
     * This method is used to join multiple channels with keys.
     * @param channels
     * @param keys
     */
    public void join( String[ ] channels, String[ ] keys ) {
        String message = "";
        int index = 0;
        for ( String channel : channels ) {
            index++;
            message += channel + (index  != channels.length ? "," : "");            
        }

        if ( keys != null ) {
            message += " ";
            index = 0;

            for ( String key : keys ) {
                index++;
                message += key + (index  != keys.length ? "," : ""); 
            }
        }
        
        sendString("JOIN " + message);
    }

    /**
     * This method is used to part a single channel.
     *
     * @param channel
     */
    public void part( String channel ) {
        part( new String[ ] { channel } );
    }

    /**
     * This method is used to part multiple channels.
     * 
     * @param channels
     */
    public void part( String[ ] channels ) {
        String message = "";
        int index = 0;
        for ( String channel : channels ) {
            index++;
            message += channel + (index  != channels.length ? "," : "");
        }

        sendString("PART " + message);
    }

    /**
     * This method returns the current nickname.
     * 
     * @return
     */
    public String getNickname( ) {
        return nickname;
    }

    /**
     * This method is used to change the current nickname.
     *
     * @param value
     */
    public void setNickname( String value ) {
        this.nickname = value;
        
        if ( isConnected( ) )
            sendString("NICK " + nickname);
    }

    /**
     * This method returns the current real name.
     * 
     * @return
     */
    public String getRealName( ) {
        return realName;
    }

    /**
     * This method sets the current real name.
     * 
     * @param value
     */
    public void setRealName( String value ) {
        realName = value;
    }

    /**
     * This method returns the current client version.
     * 
     * @return
     */
    public String getClientVersion( ) {
        return clientVersion;
    }

    /**
     * This method sets the current client version.
     * 
     * @param value
     */
    public void setClientVersion( String value ) {
        clientVersion = value;
    }

    public void handleMessage( String data ) {


    }

    public void run( ) {
        // Loop while connected.
        while ( isConnected( ) ) {
            try {
                //
                String line = reader.readLine( );
                //System.out.println( line );

                //
                Pattern miscPattern = Pattern.compile("(\\S+) :(\\S+)");
                Matcher misc = miscPattern.matcher( line );
                if ( misc.matches( ) ) {
                    String command = misc.group( 1 );
                    String parameter = misc.group( 2 );

                    if ( command.equalsIgnoreCase("PING") ) {
                        sendString("PONG :" + parameter);                        
                        continue;
                    }
                }
                
                // Process server events.
                Pattern messagePattern = Pattern.compile("^:?(\\S+) :?(\\S+) :?\\s?(\\S+)\\s?:?(.*)?");
                Matcher serverMessage = messagePattern.matcher( line );
                if ( serverMessage.find( ) ) {
                    String prefix = serverMessage.group( 1 );
                    String senderID = prefix;
                    String command = serverMessage.group( 2 );
                    String parameter = serverMessage.group( 3 );
                    String message = "";

                    Pattern nickPattern = Pattern.compile("^:(\\S+)!");
                    Matcher nick = nickPattern.matcher( line );
                    if ( nick.find( ) ) senderID = nick.group( 1 );

                    if ( serverMessage.groupCount( ) > 3 ) message = serverMessage.group( 4 );

                    // Handle CTCP events.
                    if ( command.equalsIgnoreCase("PRIVMSG") && message.startsWith("\u0001") ) {
                        if ( message.equals("\u0001VERSION\u0001") ) {
                            sendNotice(senderID, "VERSION " + clientVersion);
                            continue;
                        }
                        else {
                            sendEvent( new CTCPMessageEvent(senderID, message.replace("\u0001", "")) );
                            continue;
                        }
                    }

                    // Handle INVITE events.
                    if ( command.equalsIgnoreCase("INVITE") ) {
                        sendEvent( new InviteEvent(message, senderID) );
                        continue;
                    }
                    
                    // Handle join events.
                    if ( command.equalsIgnoreCase("JOIN") ) {
                        sendEvent( new JoinEvent(parameter, senderID) );
                        continue;
                    }

                    // Handle notice events.
                    if ( command.equalsIgnoreCase("NOTICE") ) {
                        sendEvent( new NoticeEvent(senderID, message) );
                        continue;
                    }

                    // Handle PART events.
                    if ( command.equalsIgnoreCase("PART") ) {
                        sendEvent( new PartEvent(parameter, senderID) );
                        continue;
                    }
                    // Handle private message events.
                    if ( command.equalsIgnoreCase("PRIVMSG") ) {                        
                        if ( parameter.equalsIgnoreCase(nickname) )
                            sendEvent( new PrivateMessageEvent(senderID, message) );
                        else
                            sendEvent( new ChannelMessageEvent(senderID, message, parameter) );

                        continue;
                    }

                    // Handle QUIT events.
                    if ( command.equalsIgnoreCase("QUIT") ) {
                        sendEvent( new QuitEvent(senderID, parameter) );
                    }

                    // Handle IRC messages
                    int commandID = Integer.parseInt( command );

                    switch ( commandID ) {
                    case RPL_MOTDSTART:
                    case RPL_MOTD:
                    case RPL_ENDOFMOTD:
                        sendEvent( new MOTDEvent( message ) );
                        break;
                    }
                }
                else {
                    System.out.println("-------- message not parsed -----");
                    System.out.println( line );
                }
            }
            catch ( Exception ex ) {
                
            }
        }
    }
}
